﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace DungeonGenerator.Java
{
	using System.Diagnostics;

	public class Generator
	{

		// define the %chance to generate either a room or a corridor on the map
		// BTW, rooms are 1st priority so actually it's enough to just define the chance
		// of generating a room
		const int ChanceRoom = 75;

		// misc. messages to print
		const string MsgXSize = "X size of dungeon: \t";

		const string MsgYSize = "Y size of dungeon: \t";

		const string MsgMaxObjects = "max # of objects: \t";

		const string MsgNumObjects = "# of objects made: \t";

		readonly IRandomize _rnd;

		readonly Action<string> _logger;

		readonly bool _allowparallel;

		// max size of the map
		int xmax = 80; // columns

		int ymax = 25; // rows

		// size of the map
		int _xsize;
		int _ysize;

		// number of "objects" to generate on the map
		int _objects;

		// our map
		Tile[] _dungeonMap = { };
		int[] _cellContents;		// gold for now

		int _currentFeatures;

		public Generator(IRandomize rnd, Action<string> logger, bool allowparallel = false)
		{
			_rnd = rnd;
			_logger = logger;
			_allowparallel = allowparallel;
		}

		enum NavigationState
		{
			NeedsUpstairs,
			NeedsDownstairs,
			Done
		}

		public int Corridors
		{
			get;
			private set;
		}

		public int Columns { get { return this._xsize; } }

		public int Rows { get { return this._ysize; } }

		public int CurrentFeatures
		{
			get
			{
				return this._currentFeatures;
			}
		}

		public static bool IsWall(int x, int y, int xlen, int ylen, int xt, int yt, Direction d)
		{
			Func<int, int, int> a = GetFeatureLowerBound;

			Func<int, int, int> b = IsFeatureWallBound;
			switch (d)
			{
				case Direction.North:
					return xt == a(x, xlen) || xt == b(x, xlen) || yt == y || yt == y - ylen + 1;
				case Direction.East:
					return xt == x || xt == x + xlen - 1 || yt == a(y, ylen) || yt == b(y, ylen);
				case Direction.South:
					return xt == a(x, xlen) || xt == b(x, xlen) || yt == y || yt == y + ylen - 1;
				case Direction.West:
					return xt == x || xt == x - xlen + 1 || yt == a(y, ylen) || yt == b(y, ylen);
			}

			throw new InvalidOperationException();
		}

		public static int GetFeatureLowerBound(int c, int len)
		{
			return c - len / 2;
		}

		public static int IsFeatureWallBound(int c, int len)
		{
			return c + (len - 1) / 2;
		}

		public static int GetFeatureUpperBound(int c, int len)
		{
			return c + (len + 1) / 2;
		}

		public static IEnumerable<PointI> GetCorridorPoints(int x, int y, int len, Direction d)
		{
			switch (d)
			{
				case Direction.North:
					for (var yt = y; yt > y - len; yt--) yield return new PointI { X = x, Y = yt };
					break;
					case Direction.East:
					for (var xt = x; xt < x + len; xt++) yield return new PointI { X = xt, Y = y };
					break;
					case Direction.South:
					for (var yt = y; yt < y + len;yt++ ) yield return new PointI { X = x, Y = yt };
					break;
					case Direction.West:
					for (var xt = x; xt > x - len; xt--) yield return new PointI { X = xt, Y = y };
					break;
			}
		}

		public static IEnumerable<PointI> GetRoomPoints(int x, int y, int xlen, int ylen, Direction d)
		{
			// north and south share the same x strategy
			// east and west share the same y strategy
			Func<int, int, int> a = GetFeatureLowerBound;
			Func<int, int, int> b = GetFeatureUpperBound;

			switch (d)
			{
				case Direction.North:
					for (var xt = a(x, xlen); xt < b(x, xlen); xt++) for (var yt = y; yt > y - ylen; yt--) yield return new PointI { X = xt, Y = yt };
					break;
				case Direction.East:
					for (var xt = x; xt < x + xlen; xt++) for (var yt = a(y, ylen); yt < b(y, ylen); yt++) yield return new PointI { X = xt, Y = yt };
					break;
				case Direction.South:
					for (var xt = a(x, xlen); xt < b(x, xlen); xt++) for (var yt = y; yt < y + ylen; yt++) yield return new PointI { X = xt, Y = yt };
					break;
				case Direction.West:
					for (var xt = x; xt > x - xlen; xt--) for (var yt = a(y, ylen); yt < b(y, ylen); yt++) yield return new PointI { X = xt, Y = yt };
					break;
				default:
					yield break;
			}
		}
		

		public Tile GetCellType(int x, int y)
		{
			try
			{
				return this._dungeonMap[GetFlatIndex(x, y)];
			}
			catch (IndexOutOfRangeException)
			{
				new { x, y }.Dump("exceptional");
				throw;
			}
		}

		public int GetCellContents(int x, int y)
		{
			return this._cellContents[GetFlatIndex(x, y)];
		}

		public int GetRand(int min, int max)
		{
			return _rnd.Next(min, max);
		}

		public bool MakeCorridor(int x, int y, int length, Direction direction)
		{
			if (x < 0 || x > this._xsize) return false;

			// define the dimensions of the corridor (er.. only the width and height..)
			int len = this.GetRand(2, length);
			var points = GetCorridorPoints(x, y, len, direction).ToArray();
			if (points.Any(p => !InBounds(p) || this.GetCellType(p.X, p.Y) != Tile.Unused))
			{
				return false;
			}

			foreach(var p in points)
				this.SetCell(p.X,p.Y, Tile.Corridor);
			Corridors++;

			// woot, we're still here! let's tell the other guys we're done!!
			return true;
		}

		public IEnumerable<Tuple<PointI, Direction>> GetSurroundingPoints(PointI v)
		{
			var points = new[]
                             {
                                 Tuple.Create(new PointI { X = v.X, Y = v.Y + 1 }, Direction.North),
                                 Tuple.Create(new PointI { X = v.X - 1, Y = v.Y }, Direction.East),
                                 Tuple.Create(new PointI { X = v.X, Y = v.Y - 1 }, Direction.South),
                                 Tuple.Create(new PointI { X = v.X + 1, Y = v.Y }, Direction.West),
                             };
			return points.Where(p => InBounds(p.Item1));
		}

		public IEnumerable<Tuple<PointI, Direction, Tile>> GetSurroundings(PointI v)
		{
			return
					this.GetSurroundingPoints(v)
							.Select(r => Tuple.Create(r.Item1, r.Item2, this.GetCellType(r.Item1.X, r.Item1.Y)));
		}

		public bool InBounds(int x, int y)
		{
			return x > 0 && x < this.xmax && y > 0 && y < this.ymax;
		}

		public bool InBounds(PointI v)
		{
			return this.InBounds(v.X, v.Y);
		}

		public bool MakeRoom(int x, int y, int xlength, int ylength, Direction direction)
		{
			Debug.Assert(xlength > 0);
			Debug.Assert(ylength > 0);
			// define the dimensions of the room, it should be at least 4x4 tiles (2x2 for walking on, the rest is walls)
			int xlen = this.GetRand(4, xlength);
			int ylen = this.GetRand(4, ylength);

			// the tile type it's going to be filled with
			const Tile Floor = Tile.DirtFloor;

			const Tile Wall = Tile.DirtWall;
			// choose the way it's pointing at

			var points = GetRoomPoints(x, y, xlen, ylen, direction).ToArray();

			// Check if there's enough space left for it
			if (
					points.Any(
							s =>
							s.Y < 0 || s.Y > this._ysize || s.X < 0 || s.X > this._xsize || this.GetCellType(s.X, s.Y) != Tile.Unused)) return false;
			_logger(
								string.Format(
										"Making room:int x={0}, int y={1}, int xlength={2}, int ylength={3}, int direction={4}",
										x,
										y,
										xlength,
										ylength,
										direction));

			foreach (var p in points)
			{
				this.SetCell(p.X, p.Y, IsWall(x, y, xlen, ylen, p.X, p.Y, direction) ? Wall : Floor);
			}

			// yay, all done
			return true;
		}

		public Tile[] GetDungeon()
		{
			return this._dungeonMap;
		}

		public char GetCellTile(int x, int y)
		{
			switch (GetCellType(x, y))
			{
				case Tile.Unused:
					return '_';
				case Tile.DirtWall:
					return '+';
				case Tile.DirtFloor:
					return '.';
				case Tile.StoneWall:
					return 'O';
				case Tile.Corridor:
					return '#';
				case Tile.Door:
					return 'D';
				case Tile.Upstairs:
					return '<';
				case Tile.Downstairs:
					return '>';
				case Tile.Chest:
					return '*';
				default:
					throw new ArgumentOutOfRangeException("x,y");
			}
		}

		//used to print the map on the screen
		public void ShowDungeon()
		{
			for (int y = 0; y < this._ysize; y++)
			{
				for (int x = 0; x < this._xsize; x++)
				{
					Console.Write(GetCellTile(x, y));
				}

				if (this._xsize <= xmax) Console.WriteLine();
			}
		}

		public Direction RandomDirection()
		{
			int dir = this.GetRand(0, 4);
			switch (dir)
			{
				case 0:
					return Direction.North;
				case 1:
					return Direction.East;
				case 2:
					return Direction.South;
				case 3:
					return Direction.West;
				default:
					throw new InvalidOperationException();
			}
		}

		//and here's the one generating the whole map
		public bool CreateDungeon(int inx, int iny, int inobj)
		{
			this._objects = inobj < 1 ? 10 : inobj;

			this.SetTargetSize(inx, iny);

			Console.WriteLine(MsgXSize + this._xsize);
			Console.WriteLine(MsgYSize + this._ysize);
			Console.WriteLine(MsgMaxObjects + this._objects);

			// redefine the map var, so it's adjusted to our new map size
			this._dungeonMap = new Tile[this._xsize * this._ysize];
			this._cellContents = new int[this._xsize * this._ysize];

			// start with making the "standard stuff" on the map
			this.Initialize();

			/*******************************************************************************
			And now the code of the random-map-generation-algorithm begins!
			*******************************************************************************/

			// start with making a room in the middle, which we can start building upon
			this.MakeRoom(this._xsize / 2, this._ysize / 2, 8, 6, RandomDirection());

			// keep count of the number of "objects" we've made
			int currentFeatures = 1; // +1 for the first room we just made

			this._currentFeatures = currentFeatures = this.CreateFeatures(currentFeatures);

			/*******************************************************************************
			All done with the building, let's finish this one off
			*******************************************************************************/
			this.AddNavigation();

			this.AddFlair(12);
			// all done with the map generation, tell the user about it and finish
			Console.WriteLine(MsgNumObjects + currentFeatures);

			return true;
		}

		int CreateFeatures(int currentFeatures)
		{
			// then we start the main loop
			for (int countingTries = 0; countingTries < 1000; countingTries++)
			{
				// check if we've reached our quota
				if (currentFeatures == this._objects)
				{
					break;
				}

				var nextSite = this.TryFindFeatureBuildSite();
				if (nextSite != null)
				{
					currentFeatures = this.CreateFeature(nextSite, currentFeatures);
				}
			}
			return currentFeatures;
		}

		FeatureSite TryFindFeatureBuildSite()
		{
			//var source = ;

			// 1000 chances to find a suitable object (room or corridor)..
			var q = from p in this.FindPotentialFeatureSites()
							
			        let sur = this.GetSurroundings(p)
							// check that we haven't got another door nearby, so we won't get alot of openings besides
			        // each other
			        where sur.All(s => s.Item3 != Tile.Door)
							// check if we can reach the place
			        let canReach = sur.FirstOrDefault(s => s.Item3 == Tile.Corridor || s.Item3 == Tile.DirtFloor)
			        where canReach != null
			        select new FeatureSite(p.X, p.Y, canReach.Item2);

			return q.FirstOrDefault();			 
		
		}

		IEnumerable<PointI> FindPotentialFeatureSites()
		{
			var q = from _ in _allowparallel ? Enumerable.Range(0, 1000).AsParallel() : Enumerable.Range(0, 1000)
			        let newx = this.GetRand(1, this._xsize - 1)
			        let newy = this.GetRand(1, this._ysize - 1)
			        let currentCell = this.GetCellType(newx, newy)
			        where currentCell == Tile.DirtWall || currentCell == Tile.Corridor
			        select new PointI { X = newx, Y = newy };
			return q;
		} 

		int CreateFeature(FeatureSite site, int currentFeatures)
		{
			// choose what to build now at our newly found place, and at what direction
			int feature = this.GetRand(0, 100);
			if (feature <= ChanceRoom)
			{
				// a new room
				if (this.MakeRoom(site.newx + site.xmod, site.newy + site.ymod, 8, 6, site.Dir))
				{
					currentFeatures++; // add to our quota

					// then we mark the wall opening with a door
					this.SetCell(site.newx, site.newy, Tile.Door);

					// clean up infront of the door so we can reach it
					this.SetCell(site.newx + site.xmod, site.newy + site.ymod, Tile.DirtFloor);
				}
			}
			else if (feature >= ChanceRoom)
			{
				// new corridor
				if (this.MakeCorridor(site.newx + site.xmod, site.newy + site.ymod, 6, site.Dir))
				{
					// same thing here, add to the quota and a door
					currentFeatures++;

					this.SetCell(site.newx, site.newy, Tile.Door);
				}
			}
			return currentFeatures;
		}

		void SetTargetSize(int inx, int iny)
		{
			// adjust the size of the map, if it's smaller or bigger than the limits
			_xsize = NormalizeTargetSize(inx, 3, xmax);
			_ysize = NormalizeTargetSize(iny, 3, ymax);
		}

		static int NormalizeTargetSize(int inc, int min, int max)
		{
			if (inc < min) return min;
			if (inc > max) return max;
			return inc;
		}

		void Initialize()
		{
			for (int y = 0; y < this._ysize; y++)
			{
				for (int x = 0; x < this._xsize; x++)
				{
					var wall = y == 0 || y == this._ysize - 1 || x == 0 || x == this._xsize - 1;
					// ie, making the borders of unwalkable walls
					this.SetCell(x, y, wall ? Tile.StoneWall : Tile.Unused);
				}
			}
		}

		// setting a tile's type
		void SetCell(int x, int y, Tile celltype)
		{
			this._dungeonMap[GetFlatIndex(x, y)] = celltype;
		}

		////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		
		void AddFlair(int desiredPieces)
		{
			// sprinkle out the bonusstuff (stairs, chests etc.) over the map
			var maxAttempts = xmax * ymax;
			var attempts = 0;
			var pieces = 0;
			while (attempts++ < maxAttempts && pieces < desiredPieces)
			{
				if (TryPlaceFlair()) pieces++;
			}
		}

		int GetFlatIndex(int x, int y)
		{
			return x + this._xsize * y;
		}

		bool TryPlaceFlair()
		{
			var x = _rnd.Next(1, _xsize);
			var y = _rnd.Next(1, _ysize);
			if (this.IsWalkableCell(x, y))
			{
				this._cellContents[this.GetFlatIndex(x,y)] += 5;
				return true;
			}
			return false;
		}

		private bool IsWalkableCell(int x, int y)
		{
			var tile = this.GetCellType(x, y);
			return tile != Tile.DirtWall && tile != Tile.StoneWall;
		}

		void AddNavigation()
		{
			
			var state = NavigationState.NeedsUpstairs; // the state the loop is in, start with the stairs
			var maxAttempts = xmax * ymax;
			int attempts = 0;
			while (state != NavigationState.Done && attempts < maxAttempts)
			{
				attempts++;
				state = this.TryPlaceNavigation(state);
			}

			if (attempts >= maxAttempts)
			{
				throw new InvalidOperationException();
			}
		}

		NavigationState TryPlaceNavigation(NavigationState state)
		{
			for (var testing = 0; testing < 1000; testing++)
			{
				var newx = this.GetRand(1, this._xsize - 1);
				var newy = this.GetRand(1, this._ysize - 2);

				// check if we can reach the spot
				var surrounds = this.GetSurroundings(new PointI { X = newx, Y = newy });
				if (surrounds.Any(s => s.Item3 != Tile.DirtFloor && s.Item3 != Tile.Corridor))
				{
					continue;
				}

				this.SetCell(newx, newy, state == NavigationState.NeedsUpstairs ? Tile.Upstairs : Tile.Downstairs);
				state = state == NavigationState.NeedsUpstairs ? NavigationState.NeedsDownstairs : NavigationState.Done;
				break;
			}
			return state;
		}

	}
}
